<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Map</title>
</head>
<body>
<script type="text/javascript">
    {
        /**
         * Map结构的目的和基本用法
         * JavaScript的对象(Object)，本质上是键值对的集合(Hash结构)，但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
         */
        let map = new Map();
        const obj = {value: 'hello world'};
        map.set(obj, 'content');
        console.log(map.get(obj)); // content

        /*作为构造函数，Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。*/
        let m = new Map([
            ['name', '张三'],
            ['title', 'Author'],
            [true, 'foo'],
            ['true', 'bar']
        ]);
        console.log(m.size); // 4
        console.log(m.has("name")); // true
        console.log(m.get("name")); // 张三
        console.log(m.get(true)); // foo
        console.log(m.get('true')); // bar

        /*如果对同一个键多次赋值，后面的值将覆盖前面的值*/
        let cover = new Map();
        cover.set(1, 'aaa').set(1, 'bbb');
        console.log(cover.get(1)); // bbb
        let k1 = ['a'];
        let k2 = ['a'];
        cover.set(k1, 111);
        cover.set(k2, 222);
        console.log(cover.get(k1)); // 111
        console.log(cover.get(k2)); // 222 变量k1和k2的值是一样的，但是它们在Map结构中被视为两个键

        /*Map的键实际上是跟内存地址绑定的，只要内存地址不一样，就视为两个键。这就解决了同名属性碰撞(clash)的问题，我们扩展别人的库的时候，如果使用对象作为键名，就不用担心自己的属性与原作者的属性同名*/

        /*如果Map的键是一个简单类型的值(数字、字符串、布尔值)，则只要两个值严格相等，Map将其视为一个键，包括0和-0。另外，虽然NaN不严格相等于自身，但Map将其视为同一个键*/
        cover.set(NaN, 123);
        console.log(cover.get(NaN)); // 123
        cover.set(-0, 333);
        console.log(cover.get(+0)); // 333
    }
    {
        /**
         * 实例的属性和操作方法
         * 1、size：返回Map结构的成员总数
         * 2、set(key, value)：设置key所对应的键值，然后返回整个Map结构。如果key已经有值，则键值会被更新，否则就新生成该键
         * 3、get(key)：读取key对应的键值，如果找不到key，返回undefined
         * 4、has(key)：返回一个布尔值，表示某个键是否在Map数据结构中
         * 5、delete(key)：删除某个键，返回true。如果删除失败，返回false
         * 6、clear()：清除所有成员，没有返回值
         */
        /*1、size*/
        let map = new Map();
        map.set('foo', true);
        map.set('bar', false);
        console.log(map.size); // 2

        /*2、set(key, value)*/
        map.set(1, 'a').set(2, 'b').set(3, 'c');
        console.log(map); // Map(5) {"foo" => true, "bar" => false, 1 => "a", 2 => "b", 3 => "c"}

        /*3、get(key)*/
        console.log(map.get('foo')); // true

        /*4、has(key)*/
        console.log(map.has('1')); // false
        console.log(map.has(1)); // true

        /*5、delete(key)*/
        map.delete(3);
        console.log(map); // Map(4) {"foo" => true, "bar" => false, 1 => "a", 2 => "b"}

        /*6、clear()*/
        map.clear();
        console.log(map.size); // 0
    }
    {
        /**
         * 遍历方法
         * keys()：返回键名的遍历器
         * values()：返回键值的遍历器
         * entries()：返回所有成员的遍历器
         * forEach()：遍历Map的所有成员
         */
        /*1、keys()，values()，entries()。Map的遍历顺序就是插入顺序*/
        let map = new Map([['F', 'no'], ['T', 'yes']]);
        for (let key of map.keys()) {
            console.log(key); // F T
        }
        for (const value of map.values()) {
            console.log(value); // no yea
        }
        for (const [key, value] of map.entries()) {
            console.log(key, value); // F no T yes
        }
        console.log(map[Symbol.iterator] === map.entries); // true

        /*Map结构转为数组结构，比较快速的方法是结合使用扩展运算符(...)*/
        console.log([...map.keys()]); // ["F", "T"]
        console.log([...map.values()]); // ["no", "yes"]
        console.log([...map.entries()]); // ["F", "no"] ['T', 'yes']
        console.log([...map]); // ["F", "no"] ['T', 'yes']

        /*结合数组的map方法、filter方法，可以实现Map的遍历和过滤(Map本身没有map和filter方法)*/
        const m = new Map().set(1, 'aaa').set(2, 'bbb').set(3, 'ccc');
        console.log(new Map([...m].filter(([k, v]) => k < 3))); // {1 => "aaa", 2 => "bbb"}
        console.log(new Map([...m].map(([k, v]) => [k * 2, '_' + v]))); // {2 => "_aaa", 4 => "_bbb", 6 => "_ccc"}

        /*Map还有一个forEach方法，与数组的forEach方法类似，也可以实现遍历*/
        map.forEach(function (value, key, map) {
            console.log("Key: %s, Value: %s", key, value, `map：${map}`); // Key: F, Value: no  Key: T, Value: yes  map：[object Map]
        });

        /*forEach方法还可以接受第二个参数，用来绑定this*/
        let reporter = {
            report: function (key, value) {
                console.log(`Key: ${key}, Value: ${value}`); // Key: F, Value: no  Key: T, Value: yes
            }
        };
        map.forEach(function (value, key, map) {
            this.report(key, value); // forEach方法的回调函数的this，就指向reporter
        }, reporter);
    }
    {
        /**与其他数据结构的互相转换*/
        /*1、Map转为数组：Map转为数组最方便的方法，就是使用扩展运算符(...)*/
        let mapToArray = new Map().set(true, 7).set({foo: 3}, ['abc']);
        console.log([...mapToArray]); // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

        /*2、数组转为Map：将数组转入Map构造函数，就可以转为Map*/
        let arrayToMap = [[true, 7], [{foo: 3}, ['abc']]];
        console.log(new Map(arrayToMap)); // Map(2) {true => 7, {foo: 3} => ['abc']}

        /*3、Map转为对象：如果所有Map的键都是字符串，它可以转为对象*/
        function strMapToObj(strMap) {
            let obj = Object.create(null);
            for (let [k, v] of strMap) {
                obj[k] = v;
            }
            return obj;
        }

        let mapToObject = new Map().set('yes', true).set('no', false);
        console.log(strMapToObj(mapToObject)); // {yes: true, no: false}

        /*4、对象转为Map*/
        function objToStrMap(obj) {
            let strMap = new Map();
            for (let k of Object.keys(obj)) {
                strMap.set(k, obj[k]);
            }
            return strMap;
        }

        console.log(objToStrMap({yes: true, no: false})); // Map(2) {"yes" => true, "no" => false}

        /*5、Map转为JSON*/

        /*Map转为JSON要区分两种情况。一种情况是，Map的键名都是字符串，这时可以选择转为对象JSON*/
        function strMapToJson(strMap) {
            return JSON.stringify(strMapToObj(strMap));
        }

        let objToJson = new Map().set('yes', true).set('no', false);
        console.log(strMapToJson(objToJson)); // {"yes":true,"no":false}

        /*另一种情况是，Map的键名有非字符串，这时可以选择转为数组JSON*/
        function mapToArrayJson(map) {
            return JSON.stringify([...map]);
        }

        let arrayToJson = new Map().set(true, 7).set({foo: 3}, ['abc']);
        console.log(mapToArrayJson(arrayToJson)); // [[true,7],[{"foo":3},["abc"]]]

        /*6、JSON转为Map：正常情况下，所有键名都是字符串*/
        function jsonToStrMap(jsonStr) {
            return objToStrMap(JSON.parse(jsonStr));
        }

        console.log(jsonToStrMap('{"yes":true,"no":false}')); // Map(2) {"yes" => true, "no" => false}

        /*但是，有一种特殊情况，整个JSON就是一个数组，且每个数组成员本身，又是一个有两个成员的数组。这时，它可以一一对应地转为Map。这往往是数组转为JSON的逆操作*/
        function jsonToMap(jsonStr) {
            return new Map(JSON.parse(jsonStr));
        }

        console.log(jsonToMap('[[true,7],[{"foo":3},["abc"]]]')); // Map {true => 7, Object {foo: 3} => ['abc']}
    }
    {
        /**
         * WeakMap：与Map结构基本类似，唯一的区别是它只接受对象作为键名(null除外)，不接受其他类型的值作为键名，而且键名所指向的对象，不计入垃圾回收机制。
         * 一是没有遍历操作（即没有key()、values()和entries()方法），也没有size属性
         * 二是无法清空，即不支持clear方法。这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。因此，WeakMap只有四个方法可用：get()、set()、has()、delete()。
         */
        let weakMap = new WeakMap();
        const obj = {};
        weakMap.set(obj, 'abc');
        console.log(weakMap.get(obj)); // abc
    }
</script>
</body>
</html>